/*
Copyright (c) 2008  Franklin Schmidt <fschmidt@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package fschmidt.util.java;

import java.io.Externalizable;
import java.io.ObjectInput;
import java.io.IOException;
import java.io.ObjectOutput;
import java.io.ObjectInputStream;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.ResultSet;
import java.math.BigDecimal;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import fschmidt.db.DbStorable;


public final class Money implements Comparable, Externalizable, DbStorable {
	private static final int currentVersion = 0;
	private static final long serialVersionUID = 0L;

	private /*final*/ long cents;

	public static Money read(ObjectInput in)
		throws IOException
	{
		if( !in.readBoolean() )
			return null;
		return new Money(in.readLong());
	}

	public static void write(ObjectOutput out,Money m)
		throws IOException
	{
		if( m==null ) {
			out.writeBoolean(false);
			return;
		}
		out.writeBoolean(true);
		out.writeLong(m.cents);
	}

	public void writeExternal(ObjectOutput out)
		throws IOException
	{ 
		out.writeInt(currentVersion);
		out.writeLong(cents);
	}

	public void readExternal(ObjectInput in)
		throws IOException, ClassNotFoundException
	{
		int ver = in.readInt();
		switch(ver) {
		case 0:
			cents = in.readLong();
			break;
		default:
			throw new RuntimeException();
		}
	}

	public Money() {}  // for Externalizable

	public Money(ObjectInput in)
		throws IOException, ClassNotFoundException
	{
		cents = in.readInt();
	}

	public Money(long cents) {
		this.cents = cents;
	}

	private static final Pattern p = Pattern.compile(" *-?\\$? *[0-9,.]+ *");
	private static final Pattern pSub = Pattern.compile("[ \\$,]");

	public Money(String val)
		throws NumberFormatException
	{
		if( !p.matcher(val).matches() )
			throw new NumberFormatException("invalid format for money: '"+val+"'");
		Matcher mSub = pSub.matcher(val);
		val =  pSub.matcher(val).replaceAll("");
		cents = Math.round(Float.parseFloat(val)*100);
	}

	public Money(double d) {
		cents = (long)Math.round( (d *1000) / 10);
	}

	public void setField(PreparedStatement stmt,int idx)
		throws SQLException
	{
		stmt.setBigDecimal(idx,toBigDecimal());
	}

	public static void setMoneyNull(PreparedStatement stmt,int idx)
		throws SQLException
	{
		stmt.setNull(idx,java.sql.Types.DECIMAL);
	}

	public static void setMoney(PreparedStatement stmt,int idx,Money m)
		throws SQLException
	{
		if( m==null ) {
			setMoneyNull(stmt,idx);
		} else {
			m.setField(stmt,idx);
		}
	}

	public static Money getMoney(ResultSet rs,String columnName)
		throws SQLException
	{
		Money m = new Money(rs,columnName);
		return rs.wasNull() ? null : m;
	}

	private Money(ResultSet rs,String columnName)
		throws SQLException
	{
		cents = Math.round(rs.getFloat(columnName)*100);
	}

	public boolean equals(Object obj) {
		if( obj==null || !(obj instanceof Money) )
			return false;
		Money m = (Money)obj;
		return cents==m.cents;
	}

	public int compareTo(Money val) {
		return val != null ?
				(cents < val.cents ? -1 : cents==val.cents ? 0 : 1) :
				0;
	}

	public int compareTo(Object obj) {
		return compareTo( (Money)obj );
	}

	public int hashCode() {
		return (int)cents;
	}

	public String toString() {
		return toString("$");
	}

	public String toString(String currency) {
		StringBuilder buf = new StringBuilder();
		buf.append( cents<0 ? -cents : cents );
		while( buf.length() < 3 )
			buf.insert(0,'0');
		buf.insert(buf.length()-2,'.');
		int start = buf.charAt(0)=='-' ? 1 : 0;
		for( int i=buf.length()-6; i>start; i-=3 )
			buf.insert(i,',');
		buf.insert(0,currency);
		if( cents < 0 )
			buf.insert(0,'-');
		return buf.toString();
	}

	public String toSimpleString() {
		StringBuilder buf = new StringBuilder();
		buf.append(cents);
		while( buf.length() < 3 )
			buf.insert(0,'0');
		buf.insert(buf.length()-2,'.');

		return buf.toString();
	}	

	public boolean isZero() {
		return cents==0;
	}

	public Money min(Money val) {
		return cents < val.cents ? this : val;
	}

	public Money max(Money val) {
		return cents > val.cents ? this : val;
	}

	public Money add(Money val) {
		return new Money(cents+val.cents);
	}

	public Money subtract(Money val) {
		return new Money(cents-val.cents);
	}

	public Money multiply(int val) {
		return new Money(cents*val);
	}

	public Money multiply(float val) {
		return new Money(Math.round(cents*val));
	}

	public Money multiply(double val) {
		return new Money((long)Math.round(cents*val));
	}

	public double divide(Money val) {
		if ((double)val.cents == 0) {
		    return Double.NaN;
		}
		return (double)cents/(double)val.cents;
	}

	public Money divide(int val) {
		return new Money(cents/val);
	}

	public Money divideRoundingUp(int val) {
		return new Money((cents+val-1)/val);
	}

	public Money divide(float val) {
		return new Money(Math.round(cents/val));
	}

	public Money divide(double val) {
		return new Money((long)Math.round(cents/val));
	}

	public Money inc() {
		return new Money(cents+1);
	}

	public Money dec() {
		return new Money(cents-1);
	}

	public Money avg(Money val) {
		return new Money((cents+val.cents)/2);
	}

	public double doubleValue() {
		return cents/100.0;
	}

	public BigDecimal toBigDecimal() {
		return BigDecimal.valueOf(cents,2);
	}

	public long getCents() {
		return cents;
	}


	public static final Money ZERO = new Money(0);
	public static final Money MAX_VALUE = new Money(Integer.MAX_VALUE);
	public static final Money MIN_VALUE = new Money(Integer.MIN_VALUE);

	private void readObject(ObjectInputStream stream)
		throws IOException, ClassNotFoundException
	{
		stream.defaultReadObject();
	}

	public static void main(String[] args) {
		System.out.println(new Money("z$1,234.56 "));
	}

}
